// Copyright 2011 Google Inc. All Rights Reserved.

package naclrun

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"path"
	"strings"
	"syscall"

	"naclrun.googlecode.com/git/pkg/imc"
	"naclrun.googlecode.com/git/pkg/udrpc"
)

const (
	NaClPluginBoundSock      = 3
	NaClPluginAsyncFromChild = 6
	NaClPluginAsyncToChild   = 7
)


// Params describes a NaCl module to run.
type Params struct {
	// Path to runnable-ld.so in nacl glibc (real path).
	RunnableLd string
	// NaCl module to run (virtual fs path).
	Module string

	// Path to sel_ldr (real path).
	SelLdr string

	// Arguments to NaCl module.
	Args []string

	// Environment for NaCl module.
	Env []string

	// If /dev/shm is not available, it makes sense to specify
	// this argument as alternative location for memory-mount fs (real path).
	Tmpfs string

	// Syscall proxy that intercepts all NaCl module requests and probably fullfil them.
	Proxy Proxy
}

// SelLdrProcess represents a running NaCl module.
type SelLdrProcess struct {
	*os.Process
}

func socketpair() (f0, f1 *os.File) {
	f0, f1, err := imc.SocketPair()
	if err != nil {
		log.Fatal("Can't create imc socket pair:", err)
	}
	return
}

type argsMessage struct {
	magic [4]byte
	argc  uint32
	envc  uint32
}

func packArgsMessage(argv, envv []string) []byte {
	buf := new(bytes.Buffer)
	buf.WriteString("ARGS")
	binary.Write(buf, binary.LittleEndian, uint32(len(argv)))
	binary.Write(buf, binary.LittleEndian, uint32(len(envv)))
	for _, arg := range argv {
		buf.WriteString(arg)
		buf.WriteByte(0)
	}
	for _, env := range envv {
		buf.WriteString(env)
		buf.WriteByte(0)
	}
	return buf.Bytes()
}

func (p *Params) spawnSelLdrWithSockets(args []string) (proc *os.Process, unused, recv, send *imc.Socket, err os.Error) {
	unused0, unused1 := socketpair()
	recv0, recv1 := socketpair()
	send0, send1 := socketpair()
	fullArgs := []string{p.SelLdr}
	add := func(fd int) {
		fullArgs = append(fullArgs, "-i", fmt.Sprintf("%d:%d", fd, fd))
	}
	add(NaClPluginBoundSock)
	add(NaClPluginAsyncFromChild)
	add(NaClPluginAsyncToChild)
	fullArgs = append(fullArgs, args...)

	var env []string
	if p.Tmpfs != "" {
		var fi *os.FileInfo
		fi, err = os.Stat(p.Tmpfs)
		if err != nil {
			err = fmt.Errorf("Can't stat --tmpfs='%s': %v", p.Tmpfs, err)
			return
		}
		if !fi.IsDirectory() {
			err = fmt.Errorf("--tmpfs='%s' is not a directory. Stat: %v", fi)
			return
		}
		env = append(env, "NACL_TMPFS_PREFIX="+p.Tmpfs+"/nacl-tmp-shm")
	}

	proc, err = os.StartProcess(p.SelLdr, fullArgs,
		&os.ProcAttr{Files: []*os.File{
			os.Stdin,
			os.Stdout,
			os.Stderr,
			3: unused0,
			NaClPluginAsyncFromChild: recv0,
			NaClPluginAsyncToChild:   send0},
			Env: env,
		})
	if err != nil {
		log.Fatal("Can't start sel_ldr: ", err)
	}
	unused = imc.NewSocket(unused1)
	recv = imc.NewSocket(recv1)
	send = imc.NewSocket(send1)
	return
}


func fileServer(recv, send *imc.Socket, proxy Proxy) {
	defer recv.Close()
	defer send.Close()
	for {
		data, handles, err := recv.Recv()
		if err != nil {
			if err != os.EOF {
				log.Print("FileServer has received a read error. Error: ", err)
			}
			return
		}
		handles.Close()
		if len(data) < 4 {
			log.Printf("FileServer has received too short (%d bytes) message. Quit", len(data))
			return
		}
		buf := bytes.NewBuffer(data)
		if !bytes.HasPrefix(buf.Bytes(), []byte("Open")) {
			log.Print("FileServer: Incoming request does not start with Open. Stops serving")
			return
		}
		buf.Next(4)

		var flags, mode uint32
		err = binary.Read(buf, binary.LittleEndian, &flags)
		if err != nil {
			log.Print("FileServer: Can't read flags for Open, err: ", err)
			return
		}
		err = binary.Read(buf, binary.LittleEndian, &mode)
		if err != nil {
			log.Printf("FileServer: Can't read mode for Open, err: ", err)
			return
		}
		filename := buf.String()
		file, err := proxy.Open(filename, int(flags), mode)

		toHandles := new(udrpc.Handles)
		to := new(bytes.Buffer)
		if err != nil {
			to.WriteString("Fail")
		} else {
			toHandles.Add(file)
		}
		err = send.Send(to.Bytes(), toHandles)
		toHandles.Close()
		if err != nil {
			// Socket has been closed. Exit.
			return
		}
	}
}

func kill(p *os.Process) os.Error {
	errno := syscall.Kill(p.Pid, syscall.SIGKILL)
	if errno != 0 {
		return os.NewSyscallError("kill", errno)
	}
	return nil
}

// Start starts the NaCl module described by the param
func Start(p *Params) (selLdr *SelLdrProcess, err os.Error) {
	selLdrArgs := []string{"-s", "--", p.RunnableLd}
	proc, unused, recv, send, err := p.spawnSelLdrWithSockets(selLdrArgs)
	if err != nil {
		return
	}
	defer unused.Close()

	go fileServer(recv, send, p.Proxy)

	// ld.so does not like paths like lala.nexe, because it tries to look in the ld cache and fails.
	nacl_module := p.Module
	if !path.IsAbs(nacl_module) && !strings.HasPrefix(nacl_module, "./") {
		nacl_module = "./" + nacl_module
	}
	argv := []string{"unused-argv0", "--library-path", "/nacl64/lib", nacl_module}
	argv = append(argv, p.Args...)
	envv := []string{"NACL_FILE_RPC=1"}
	envv = append(envv, p.Env...)
	err = send.Send(packArgsMessage(argv, envv), nil)
	if err != nil {
		kill(proc)
		return
	}
	return &SelLdrProcess{proc}, nil
}
